/*
概述

JavaScript 现在有两种模块。一种是 ES6 模块，简称 ESM；
另一种是 CommonJS 模块，简称 CJS。

CommonJS 模块是 Node.js 专用的，与 ES6 模块不兼容。
语法上面，两者最明显的差异是，CommonJS 模块使用require()和module.exports，ES6 模块使用import和export。

它们采用不同的加载方案。
从 Node.js v13.2 版本开始，Node.js 已经默认打开了 ES6 模块支持。

Node.js 要求 ES6 模块采用.mjs后缀文件名。
也就是说，只要脚本文件里面使用import或者export命令，那么就必须采用.mjs后缀名。
Node.js 遇到.mjs文件，就认为它是 ES6 模块，默认启用严格模式，不必在每个模块文件顶部指定"use strict"。

如果不希望将后缀名改成.mjs，可以在项目的package.json文件中，指定type字段为module。
*/

{
	"type": "module" //commonjs
}

/*
一旦设置了以后，该目录里面的 JS 脚本，就被解释用 ES6 模块。


如果这时还要使用 CommonJS 模块，那么需要将 CommonJS 脚本的后缀名都改成.cjs。
如果没有type字段，或者type字段为commonjs，则.js脚本会被解释成 CommonJS 模块。

总结为一句话：.mjs文件总是以 ES6 模块加载，
.cjs文件总是以 CommonJS 模块加载，
.js文件的加载取决于package.json里面type字段的设置。

注意，ES6 模块与 CommonJS 模块尽量不要混用。
require命令不能加载.mjs文件，会报错，只有import命令才可以加载.mjs文件。
反过来，.mjs文件里面也不能使用require命令，必须使用import。
*/


/*
package.json 的 main 字段

package.json文件有两个字段可以指定模块的入口文件：main和exports。
比较简单的模块，可以只使用main字段，指定模块加载的入口文件。
*/

// ./node_modules/es-module-package/package.json
{
	"type": "module",
	"main": "./src/index.js"
}

/*
上面代码指定项目的入口脚本为. / src / index.js， 它的格式为 ES6 模块。
 如果没有type字段， index.js就会被解释为 CommonJS 模块。
 */


/*
然后，import命令就可以加载这个模块。
*/

// ./my-app.mjs
import {
	something
} from 'es-module-package';
// 实际加载的是 ./node_modules/es-module-package/src/index.js 自己找模块包然后入口方式为main

/*
上面代码中，运行该脚本以后，Node.js 就会到./node_modules目录下面，寻找es-module-package模块，
然后根据该模块package.json的main字段去执行入口文件。

这时，如果用 CommonJS 模块的require()命令去加载es-module-package模块会报错，
因为 CommonJS 模块不能处理export命令
。*/


/*
exports字段的优先级高于main字段。它有多种用法。

（1）子目录别名

package.json文件的exports字段可以指定脚本或子目录的别名。
*/

// ./node_modules/es-module-package/package.json
{
	"exports": {
		"./submodule": "./src/submodule.js"
	}
}

/*
上面的代码指定src/submodule.js别名为submodule， 然后就可以从别名加载这个文件。
*/

import submodule from 'es-module-package/submodule';
// 加载 ./node_modules/es-module-package/src/submodule.js

/*
下面是子目录别名的例子。
*/

// ./node_modules/es-module-package/package.json
{
	"exports": {
		"./features/": "./src/features/"
	}
}

import feature from 'es-module-package/features/x.js';
// 加载 ./node_modules/es-module-package/src/features/x.js


/*
如果没有指定别名，就不能用“模块+脚本名”这种形式加载脚本。
必须有别名才能省略模块
*/

// 报错
import submodule from 'es-module-package/private-module.js';

// 不报错
import submodule from './node_modules/es-module-package/private-module.js';


/*
(2）main 的别名

exports字段的别名如果是.，就代表模块的主入口，优先级高于main字段，
并且可以直接简写成exports字段的值。
*/

{
	"exports": {
		".": "./main.js"
	}
}

// 等同于
{
	"exports": "./main.js"
}

/*
由于exports字段只有支持 ES6 的 Node.js 才认识，
所以可以用来兼容旧版本的 Node.js。
*/

{
	"main": "./main-legacy.cjs",
	"exports": {
		".": "./main-modern.cjs"
	},
}

/*
上面代码中，老版本的 Node.js （不支持 ES6 模块）的入口文件是main-legacy.cjs，
新版本的 Node.js 的入口文件是main-modern.cjs。
*/

/*
（3）条件加载

利用.这个别名，可以为 ES6 模块和 CommonJS 指定不同的入口。
目前，这个功能需要在 Node.js 运行的时候，打开--experimental-conditional-exports标志。
*/

{
	"type": "module",
	"exports": {
		".": {
			"require": "./main.cjs",
			"default": "./main.js"
		}
	}
}

/*
上面代码中，别名.的require条件指定require()命令的入口文件（即 CommonJS 的入口），
default条件指定其他情况的入口（即 ES6 的入口）。
*/

/*
上面的写法可以简写如下。
只要是.都可以简写
*/

{
	"exports": {
		"require": "./main.cjs",
		"default": "./main.js"
	}
}

//注意，如果同时还有其他别名，就不能采用简写，否则或报错。

{
	// 报错  这种情况不能简写要另外“.”开括号
	"exports": {
		"./feature": "./lib/feature.js",
		"require": "./main.cjs",
		"default": "./main.js"
	}
}

/*
CommonJS 模块加载 ES6 模块

CommonJS 的require()命令不能加载 ES6 模块，会报错，只能使用import()这个方法加载。
*/

(async () => {
	await import('./my-app.mjs');
})();

/*
上面代码可以在 CommonJS 模块中运行。

require()不支持 ES6 模块的一个原因是，它是同步加载，
而 ES6 模块内部可以使用顶层await命令，导致无法被同步加载。
*/

/*
ES6 模块加载 CommonJS 模块

ES6 模块的import命令可以加载 CommonJS 模块，但是只能整体加载，不能只加载单一的输出项。
*/
// 正确
import packageMain from 'commonjs-package';

// 报错
import {
	method
} from 'commonjs-package';

/*
这是因为 ES6 模块需要支持静态代码分析， 而 CommonJS 模块的输出接口是module.exports，
 是一个对象， 无法被静态分析， 所以只能整体加载。

加载单一的输出项， 可以写成下面这样。
*/
import packageMain from 'commonjs-package';
const {
	method
} = packageMain;


/*
还有一种变通的加载方法， 就是使用 Node.js 内置的module.createRequire() 方法。
*/

// cjs.cjs
module.exports = 'cjs';

// esm.mjs
import {
	createRequire
} from 'module';

const require = createRequire(
	import.meta.url);

const cjs = require('./cjs.cjs');
cjs === 'cjs'; // true

/*
上面代码中， ES6 模块通过module.createRequire() 方法可以加载 CommonJS 模块。
 但是， 这种写法等于将 ES6 和 CommonJS 混在一起了， 所以不建议使用。
 */

/*
同时支持两种格式的模块

一个模块同时要支持 CommonJS 和 ES6 两种格式，也很容易。

如果原始模块是 ES6 格式，那么需要给出一个整体输出接口，比如export default obj，
使得 CommonJS 可以用import()进行加载。

如果原始模块是 CommonJS 格式，那么可以加一个包装层。
*/
import cjsModule from '../index.js';
export const foo = cjsModule.foo;
/*
上面代码先整体输入 CommonJS 模块，然后再根据需要输出具名接口。

你可以把这个文件的后缀名改为.mjs，或者将它放在一个子目录，
再在这个子目录里面放一个单独的package.json文件，指明{ type: "module" }。

另一种做法是在package.json文件的exports字段，指明两种格式模块各自的加载入口。
*/
"exports"： {
	"require": "./index.js"，
	"import": "./esm/wrapper.js"
}

//上面代码指定require()和import，加载该模块会自动切换到不一样的入口文件。

/*
Node.js 的内置模块

Node.js 的内置模块可以整体加载，也可以加载指定的输出项。
*/

// 整体加载
import EventEmitter from 'events';
const e = new EventEmitter();

// 加载指定的输出项
import {
	readFile
} from 'fs';
readFile('./foo.txt', (err, source) => {
	if (err) {
		console.error(err);
	} else {
		console.log(source);
	}
});

/*
加载路径

ES6 模块的加载路径必须给出脚本的完整路径，不能省略脚本的后缀名。
import命令和package.json文件的main字段如果省略脚本的后缀名，会报错。
*/

// ES6 模块中将报错
import {
	something
} from './index';

//为了与浏览器的import加载规则相同，Node.js 的.mjs文件支持 URL 路径。

import './foo.mjs?query=1'; // 加载 ./foo 传入参数 ?query=1

/*
上面代码中，脚本路径带有参数?query=1，Node 会按 URL 规则解读
。同一个脚本只要参数不同，就会被加载多次，并且保存成不同的缓存。
由于这个原因，只要文件名中含有:、%、#、?等特殊字符，最好对这些字符进行转义。

目前，Node.js 的import命令只支持加载本地模块（file:协议）和data:协议，不支持加载远程模块。
另外，脚本路径只支持相对路径，不支持绝对路径（即以/或//开头的路径）。
*/

/*
内部变量

ES6 模块应该是通用的，同一个模块不用修改，就可以用在浏览器环境和服务器环境。
为了达到这个目标，Node.js 规定 ES6 模块之中不能使用 CommonJS 模块的特有的一些内部变量。

首先，就是this关键字。ES6 模块之中，顶层的this指向undefined；
CommonJS 模块的顶层this指向当前模块，这是两者的一个重大差异。

其次，以下这些顶层变量在 ES6 模块之中都是不存在的。

    arguments
    require
    module
    exports
    __filename
    __dirname
*/
